local _G = _G
local CurseProfiler = _G.CurseProfiler
local NPCs = CurseProfiler:NewModule("NPCs")

local REQUIRES = "requires" -- localize

local UnitGUID = _G.UnitGUID
local GetInstanceDifficulty = _G.GetInstanceDifficulty
local UnitName = _G.UnitName
local GetPlayerMapPosition = _G.GetPlayerMapPosition
local GetRealZoneText = _G.GetRealZoneText
local SetMapToCurrentZone = _G.SetMapToCurrentZone
local GetItemInfo = _G.GetItemInfo
local GetLootSlotLink = _G.GetLootSlotLink
local GetLootSlotInfo = _G.GetLootSlotInfo
local GetNumLootItems = _G.GetNumLootItems
local UnitClassification = _G.UnitClassification
local GetContainerItemLink = _G.GetContainerItemLink
local GetTrainerServiceTypeFilter = _G.GetTrainerServiceTypeFilter
local SetTrainerServiceTypeFilter = _G.SetTrainerServiceTypeFilter
local GetTrainerServiceCost = _G.GetTrainerServiceCost
local GetTrainerServiceSkillLine = _G.GetTrainerServiceSkillLine
local GetTrainerServiceInfo = _G.GetTrainerServiceInfo
local GetNumTrainerServices = _G.GetNumTrainerServices
local UnitPlayerControlled = _G.UnitPlayerControlled
local UnitExists = _G.UnitExists
local GetMerchantItemLink = _G.GetMerchantItemLink
local GetMerchantItemInfo = _G.GetMerchantItemInfo
local GetMerchantNumItems = _G.GetMerchantNumItems
local UnitLevel = _G.UnitLevel
local UnitReaction = _G.UnitReaction
local UnitSex = _G.UnitSex
local UnitIsTapped = _G.UnitIsTapped
local GetTime = _G.GetTime
local GetFactionInfo = _G.GetFactionInfo
local GetNumFactions = _G.GetNumFactions
local UnitIsPlayer = _G.UnitIsPlayer
local UnitFactionGroup = _G.UnitFactionGroup
local GetSpellInfo = _G.GetSpellInfo
local next = _G.next
local pairs = _G.pairs
local unpack = _G.unpack
local tonumber = _G.tonumber
local math_floor = _G.math.floor
local type = _G.type

local npcData = {}
local itemData = {}
local objectData = {}
--[===[@debug@
_G.npcData = npcData
_G.itemData = itemData
_G.objectData = objectData
--@end-debug@]===]

local FigureFaction
local FigureRequiredArenaRatingForMerchantItem
do
	local tt = _G.CreateFrame("GameTooltip")
	tt:SetOwner(_G.UIParent, "ANCHOR_NONE")
	tt.left = {}
	tt.right = {}
	for i = 1, 30 do
		tt.left[i] = tt:CreateFontString()
		tt.left[i]:SetFontObject(_G.GameFontNormal)
		tt.right[i] = tt:CreateFontString()
		tt.right[i]:SetFontObject(_G.GameFontNormal)
		tt:AddFontStrings(tt.left[i], tt.right[i])
	end
	local nextTime = 0
	local lastGUID
	local function updateTT(unit)
		local guid = UnitGUID(unit)
		local time = GetTime()
		if lastGUID == guid and nextTime < time then
			return
		end
		lastGUID = guid
		nextTime = time + 1
		tt:ClearLines()
		tt:SetUnit(unit)
		if not tt:IsOwned(_G.UIParent) then
			tt:SetOwner(_G.UIParent, "ANCHOR_NONE")
		end
	end
	
	local LEVEL_start = "^" .. (type(_G.LEVEL) == "string" and _G.LEVEL or "Level")
	
	local factionList = {}

	local PVP = type(_G.PVP) == "string" and _G.PVP or "PvP"
	function FigureFaction(unit)
		local _, faction = UnitFactionGroup(unit)
		if UnitPlayerControlled(unit) or UnitIsPlayer(unit) then
			return faction
		end

		updateTT(unit)
		local left_2 = tt.left[2]:GetText()
		local left_3 = tt.left[3]:GetText()
		if not left_2 or not left_3 then
			return faction
		end
		local hasGuild = not left_2:find(LEVEL_start)
		local factionText = not hasGuild and left_3 or tt.left[4]:GetText()
		if factionText == PVP then
			return faction
		end
		if factionList[factionText] or faction then
			return factionText
		end
	end
	
	local ITEM_REQ_ARENA_RATING_match = _G.ITEM_REQ_ARENA_RATING:gsub("%%d", "(%%d+)")
	function FigureRequiredArenaRatingForMerchantItem(index)
		lastGUID = nil
		nextTime = 0
		tt:ClearLines()
		tt:SetMerchantItem(index)
		if not tt:IsOwned(_G.UIParent) then
			tt:SetOwner(_G.UIParent, "ANCHOR_NONE")
		end
		local points
		for i = 1, tt:NumLines() do
			local text = tt.left[i]:GetText()
			local rating = text:match(ITEM_REQ_ARENA_RATING_match)
			if rating then
				points = rating+0
				break
			end
		end
		return points
	end

	local rep_list = {}

	function NPCs:UPDATE_FACTION()
		local numfactions = GetNumFactions()
--[[
		-- Lets expand all the headers
		for i = numfactions, 1, -1 do
			local name, _, _, _, _, _, _, _, _, isCollapsed = GetFactionInfo(i)
			if (isCollapsed) then
				ExpandFactionHeader(i)
				rep_list[name] = true
			end
		end
]]--
		-- Get the rep levels
		for i = 1, numfactions, 1 do
			local name, _, replevel = GetFactionInfo(i)
			factionList[name] = true
		end
--[[
		-- Collapse the headers again
		for i = numfactions, 1, -1 do
			local name = GetFactionInfo(i)
			if (rep_list[name]) then
				CollapseFactionHeader(i)
			end
		end
]]--
		lastGUID = nil
	end
	function NPCs:PLAYER_LOGIN()
		self:UPDATE_FACTION()
	end
	function NPCs:UNIT_FACTION()
		self:UPDATE_FACTION()
	end
end

local function ParseCoin(text)
	local g, s, c = text:match("([0-9]*)[^0-9]*([0-9]*)[^0-9]*([0-9]*)")
	
	if g == "" then
		return 0
	elseif s == "" then
		c = g
		s, g = 0, 0		
	elseif c == "" then
		c = s
		s = g
		g = 0
	end
	
	return c + s * 100 + g * 10000
end

local lootCD = {}
local function IsLootOffCooldown(name, isSpecial)
	if not name then
		return false
	end
	local currentTime = GetTime()
	if lootCD[name] then
		if currentTime - lootCD[name] > (isSpecial and 900 or 60) then
			lootCD[name] = currentTime
			return true
		else
			return false
		end
	else
		lootCD[name] = currentTime
		return true
	end
end

local targetLevel
local targetName

local mobIDToName = {}
local nameToMobID = {}

local function UpdateUnitInfo(unit)
	if not UnitExists(unit) or UnitPlayerControlled(unit) then
		return
	end
	local guid = UnitGUID(unit)
	local mobID = tonumber(guid:sub(-12, -7), 16)
	if mobID <= 1 then
		return
	end
	local npcData_mobID = npcData[mobID]
	if not npcData_mobID then
		npcData_mobID = {}
		npcData[mobID] = npcData_mobID
		local name = UnitName(unit)
		mobIDToName[mobID] = name
		nameToMobID[name] = mobID
	end
	
	if not UnitIsTapped(unit) then
		SetMapToCurrentZone()
		local zone = GetRealZoneText()
		local x, y = GetPlayerMapPosition("player")

		local npcData_mobID_loc = npcData_mobID.loc
		if not npcData_mobID_loc then
			npcData_mobID_loc = {}
			npcData_mobID.loc = npcData_mobID_loc
		end
		
		local npcData_mobID_loc_zone = npcData_mobID_loc[zone]
		if not npcData_mobID_loc_zone then
			npcData_mobID_loc_zone = {}
			npcData_mobID_loc[zone] = npcData_mobID_loc_zone
		end
	
		x = math_floor(x * 10000 + 0.5)
		y = math_floor(y * 10000 + 0.5)
		npcData_mobID_loc_zone[x*10001 + y] = true
	end
	
	npcData_mobID.sex = UnitSex(unit)
	npcData_mobID.faction = FigureFaction(unit)
	npcData_mobID.reaction = UnitReaction("player", unit)
	local difficulty = GetInstanceDifficulty()
	local npcData_mobID_stat = npcData_mobID["stats" .. difficulty]
	if not npcData_mobID_stat then
		npcData_mobID_stat = {}
		npcData_mobID["stats" .. difficulty] = npcData_mobID_stat
	end
	local npcData_mobID_stat_levels = npcData_mobID_stat.levels
	if not npcData_mobID_stat_levels then
		npcData_mobID_stat_levels = {}
		npcData_mobID_stat.levels = npcData_mobID_stat_levels
	end
	local level = UnitLevel(unit)
	if level < 0 then
		level = 0
	end
	npcData_mobID_stat_levels[level] = true
end

function NPCs:PLAYER_TARGET_CHANGED()
	UpdateUnitInfo('target')
	
	targetName = UnitName("target")
	targetLevel = UnitLevel("target")
end

function NPCs:PLAYER_FOCUS_CHANGED()
	UpdateUnitInfo('focus')
end

function NPCs:UNIT_TARGET(event, unit)
	UpdateUnitInfo(unit .. 'target')
end

function NPCs:UPDATE_MOUSEOVER_UNIT()
	UpdateUnitInfo('mouseover')
end

local foundNPCObject = false
local recheckMerchant
function NPCs:OnUpdate()
	local guid = UnitGUID('npc')
	local last_foundNPCObject = foundNPCObject
	foundNPCObject = guid and guid:match("^0xF13")
	if foundNPCObject and not last_foundNPCObject then
		UpdateUnitInfo('npc')
	end
	if recheckMerchant then
		self:MERCHANT_SHOW()
	end
end

local DISENCHANT = GetSpellInfo(13262)
local HERB_GATHERING = GetSpellInfo(11993)
local MINING = GetSpellInfo(32606)
local OPENING = GetSpellInfo(3365)
local PICK_POCKET = GetSpellInfo(921)
local SKINNING = GetSpellInfo(13697)
local PROSPECTING = GetSpellInfo(31252)
local FISHING = GetSpellInfo(33095)
local MILLING = GetSpellInfo(51005)
local vSpells = {
	[DISENCHANT] = 13262,
	[HERB_GATHERING] = 11993,
	[MINING] = 32606,
	[OPENING] = 3365,
	[PICK_POCKET] = 921,
	[SKINNING] = 13697,
	[PROSPECTING] = 31252,
	[FISHING] = 33095,
	[MILLING] = 51005,
}

local spellIDsFromItems = {
	[vSpells[DISENCHANT]] = true,
	[vSpells[PROSPECTING]] = true,
	[vSpells[MILLING]] = true,
}
local spellIDsFromGameObjects = {
	[vSpells[HERB_GATHERING]] = true,
	[vSpells[MINING]] = true,
	[vSpells[OPENING]] = true,
	[vSpells[FISHING]] = true,
}
local spellIDsFromNPCs = {
	[0] = true,
	[vSpells[HERB_GATHERING]] = true,
	[vSpells[MINING]] = true,
	[vSpells[PICK_POCKET]] = true,
	[vSpells[SKINNING]] = true,
}

local openedBag, openedItem, openedTime
local currentSpellID, currentSpellTarget, currentRequires
function NPCs:UNIT_SPELLCAST_SENT(event, unit, spell, rank, target)
	if unit ~= "player" then
		return
	end
	openedBag, openedItem = nil, nil
	if vSpells[spell] then
		currentSpellID = vSpells[spell]
		currentSpellTarget = target ~= "" and target or UnitName("mouseover") or _G.GameTooltipTextLeft1:GetText()
		currentRequires = nil
		if spellIDsFromGameObjects[spell] then
			for i = 1, _G.GameTooltip:NumLines() do
				local text = _G["GameTooltipTextLeft"..i]:GetText()
				if text and text:lower():match(REQUIRES) then
					currentRequires = text
				end
			end
		end
	end
end

function NPCs:UNIT_SPELLCAST_FAILED(event, unit)
	if unit ~= "player" then
		return
	end
	currentSpellID = nil
end

function NPCs:UNIT_SPELLCAST_INTERRUPTED(event, unit)
	if unit ~= "player" then
		return
	end
	currentSpellID = nil
end

function NPCs:LOOT_CLOSED()
	currentSpellID = nil
end

function NPCs:MERCHANT_SHOW()
	recheckMerchant = false
	if not UnitExists('npc') or UnitPlayerControlled('npc') then
		return
	end
	
	local guid = UnitGUID('npc')
	local mobID = tonumber(guid:sub(-12, -7), 16)
	if mobID <= 1 then
		return
	end
	local npcData_mobID = npcData[mobID]
	if not npcData_mobID then
		npcData_mobID = {}
		npcData[mobID] = npcData_mobID
	end
	
	local npcData_mobID_merchant = npcData_mobID.merchant
	if not npcData_mobID_merchant then
		npcData_mobID_merchant = {}
		npcData_mobID.merchant = npcData_mobID_merchant
	end
	
	for i = 1, GetMerchantNumItems() do
		local _, _, _, quantity, numAvailable, _, extendedCost = GetMerchantItemInfo(i)
		local link = GetMerchantItemLink(i)
		if not link then
			recheckMerchant = true
			return
		end
		if quantity < 0 then
			quantity = 1
		end
		local itemID = CurseProfiler.getShortID(link)
		npcData_mobID_merchant[itemID] = quantity
		if numAvailable ~= -1 then
			local npcData_mobID_limitedMerchant = npcData_mobID.limitedMerchant
			if not npcData_mobID_limitedMerchant then
				npcData_mobID_limitedMerchant = {}
				npcData_mobID.limitedMerchant = npcData_mobID_limitedMerchant
			end
			
			if not npcData_mobID_limitedMerchant[itemID] or numAvailable > npcData_mobID_limitedMerchant[itemID] then
				npcData_mobID_limitedMerchant[itemID] = numAvailable
			end
		end
		if extendedCost then
			local honorPoints, arenaPoints, itemCount = GetMerchantItemCostInfo(i)
			if honorPoints and honorPoints ~= 0 then
				local npcData_mobID_merchantHonor = npcData_mobID.merchantHonor
				if not npcData_mobID_merchantHonor then
					npcData_mobID_merchantHonor = {}
					npcData_mobID.merchantHonor = npcData_mobID_merchantHonor
				end
				npcData_mobID_merchantHonor[itemID] = honorPoints
			elseif arenaPoints and arenaPoints ~= 0 then
				local npcData_mobID_merchantArena = npcData_mobID.merchantArena
				if not npcData_mobID_merchantArena then
					npcData_mobID_merchantArena = {}
					npcData_mobID.merchantArena = npcData_mobID_merchantArena
				end
				npcData_mobID_merchantArena[itemID] = arenaPoints
			end
			local rating = FigureRequiredArenaRatingForMerchantItem(i)
			if rating and rating ~= 0 then
				local npcData_mobID_merchantReqRating = npcData_mobID.merchantReqRating
				if not npcData_mobID_merchantReqRating then
					npcData_mobID_merchantReqRating = {}
					npcData_mobID.merchantReqRating = npcData_mobID_merchantReqRating
				end
				npcData_mobID_merchantReqRating[itemID] = rating
			end
			if itemCount > 0 then
				for itemNum = 1, 3 do
					local itemTexture, itemValue, itemLink = GetMerchantItemCostItem(i, itemNum)
					
					if itemTexture then
						local id = itemLink:match("^|c%x%x%x%x%x%x%x%x|Hitem:(%d+):[%-%d:]*|h%[.-%]|h|r$")+0
						
						local npcData_mobID_merchantItem_num = npcData_mobID["merchantItem" .. itemNum]
						local npcData_mobID_merchantItemCost_num = npcData_mobID["merchantItemCost" .. itemNum]
						if not npcData_mobID_merchantItem_num then
							npcData_mobID_merchantItem_num = {}
							npcData_mobID_merchantItemCost_num = {}
							npcData_mobID["merchantItem" .. itemNum] = npcData_mobID_merchantItem_num
							npcData_mobID["merchantItemCost" .. itemNum] = npcData_mobID_merchantItemCost_num
						end
						npcData_mobID_merchantItem_num[itemID] = id
						npcData_mobID_merchantItemCost_num[itemID] = itemValue
					end
				end
			end
		end
	end
end

function NPCs:TRAINER_SHOW()
	if not UnitExists('npc') or UnitPlayerControlled('npc') then
		return
	end
	
	local guid = UnitGUID('npc')
	local mobID = tonumber(guid:sub(-12, -7), 16)
	if mobID <= 1 then
		return
	end
	local npcData_mobID = npcData[mobID]
	if not npcData_mobID then
		npcData_mobID = {}
		npcData[mobID] = npcData_mobID
	end
	
	local npcData_mobID_trainer = npcData_mobID.trainer
	if not npcData_mobID_trainer then
		npcData_mobID_trainer = {}
		npcData_mobID.trainer = npcData_mobID_trainer
	end
	
	local avail = GetTrainerServiceTypeFilter("available") or 0
	local unavail = GetTrainerServiceTypeFilter("unavailable") or 0
	local used = GetTrainerServiceTypeFilter("used") or 0
	SetTrainerServiceTypeFilter("available", 1)
	SetTrainerServiceTypeFilter("unavailable", 1)
	SetTrainerServiceTypeFilter("used", 1)
	
	for i = 1, GetNumTrainerServices() do
		local name, rank, category = GetTrainerServiceInfo(i)
		if category ~= "header" then
			local skill = GetTrainerServiceSkillLine(i) or ''
			local cost = GetTrainerServiceCost(i) or 0
			local found = false
			for k in pairs(npcData_mobID_trainer) do
				if k[1] == name and k[2] == rank then
					found = true
					k[3] = skill
					k[4] = cost
					break
				end
			end
			if not found then
				npcData_mobID_trainer[{ name, rank, skill, cost }] = true
			end
		end
	end
	
	SetTrainerServiceTypeFilter("available", avail)
	SetTrainerServiceTypeFilter("unavailable", unavail)
	SetTrainerServiceTypeFilter("used", used)
end

function NPCs:ITEM_LOCK_CHANGED(event, bag, item)
	openedBag, openedItem = bag, item
end

function NPCs:BAG_UPDATE()
	openedBag, openedItem = nil, nil
end

function NPCs:ACTIONBAR_HIDEGRID()
	openedBag, openedItem = nil, nil
end

local specialClassificiations = {
	rare = true,
	rareelite = true,
	worldboss = true,
}

function NPCs:LOOT_OPENED()
	local openedName, openedLink
	if openedBag and openedItem then
		local link = GetContainerItemLink(openedBag, openedItem)
		if link then
			openedName, openedLink = GetItemInfo(link)
		end
	end
	if not openedName and not targetName and (not currentSpellID or not currentSpellTarget) then
		return
	end
	if IsLootOffCooldown(openedName or targetName or currentSpellTarget, specialClassificiations[UnitClassification("target")]) then
		local loot = {}
		for i = 1, GetNumLootItems() do
			local _, name, qty = GetLootSlotInfo(i)
			local link = GetLootSlotLink(i)
			if link then
				loot[CurseProfiler.getShortID(link)] = qty
			else
				loot[0] = ParseCoin(name) 
			end
		end
		
		local spellID = currentSpellID or 0
		if openedName or spellIDsFromItems[spellID] then
			if not spellIDsFromItems[spellID] then
				spellID = vSpells[OPENING]
			else
				openedName = nil
			end
			local itemID
			if openedName then
				itemID = CurseProfiler.getShortID(openedLink)
			elseif currentSpellTarget then
				local itemName, itemLink = GetItemInfo(currentSpellTarget)
				itemID = CurseProfiler.getShortID(itemLink)
			end
			if itemID and itemID ~= 0 then
				local itemData_itemID = itemData[itemID]
				if not itemData_itemID then
					itemData_itemID = {}
					itemData[itemID] = itemData_itemID
				end
				local itemData_itemID_spellID = itemData_itemID[spellID]
				if not itemData_itemID_spellID then
					itemData_itemID_spellID = {}
					itemData_itemID[spellID] = itemData_itemID_spellID
				end
				itemData_itemID_spellID[loot] = true
			end
		else
			local targetName = currentSpellTarget or UnitName("target")
			local mobID = nameToMobID[targetName]
			if mobID and spellIDsFromNPCs[spellID] then
				local npcData_mobID = npcData[mobID]
				if not npcData_mobID then
					npcData_mobID = {}
					npcData[mobID] = npcData_mobID
				end
				local difficulty = GetInstanceDifficulty()
				local npcData_mobID_stat = npcData_mobID["stats" .. difficulty]
				if not npcData_mobID_stat then
					npcData_mobID_stat = {}
					npcData_mobID["stats" .. difficulty] = npcData_mobID_stat
				end
				local npcData_mobID_stat_loot = npcData_mobID_stat.loot
				if not npcData_mobID_stat_loot then
					npcData_mobID_stat_loot = {}
					npcData_mobID_stat.loot = npcData_mobID_stat_loot
				end
				local npcData_mobID_stat_loot_spellID = npcData_mobID_stat_loot[spellID]
				if not npcData_mobID_stat_loot_spellID then
					npcData_mobID_stat_loot_spellID = {}
					npcData_mobID_stat_loot[spellID] = npcData_mobID_stat_loot_spellID
				end
				npcData_mobID_stat_loot_spellID[loot] = true
				-- TODO: keep track of raid ID?
			elseif spellIDsFromGameObjects[spellID] then
				SetMapToCurrentZone()
				local zone = GetRealZoneText()
				local x, y = GetPlayerMapPosition("player")
				local objectData_spellID = objectData[spellID]
				if not objectData_spellID then
					objectData_spellID = {}
					objectData[spellID] = objectData_spellID
				end
				local t = {}
				objectData_spellID[t] = true
				t.x = x
				t.y = y
				t.zone = zone
				t.loot = loot
				t.requires = currentRequires
				t.target = currentSpellTarget
			end
		end
		
		openedBag, openedItem = nil, nil
		currentSpellID, currentSpellTarget = nil, nil
	end
end

local bit_band = _G.bit.band
local bit_bor = _G.bit.bor
local COMBATLOG_FILTER_NPCS = bit_bor(
	_G.COMBATLOG_OBJECT_CONTROL_NPC,
	_G.COMBATLOG_OBJECT_TYPE_NPC
)

function NPCs:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, ...)
	if bit_band(srcFlags, COMBATLOG_OBJECT_CONTROL_NPC) == 0 then
		return
	end
	local mobID = tonumber(srcGUID:sub(-12, -7), 16)
	if mobID <= 1 then
		return
	end
	local start = eventType:sub(1, 5)
	local rest = eventType:sub(7)
	local spellID
	if start == "SPELL" then
		spellID = ...
		if rest ~= "CAST_SUCCESS" and rest ~= "CAST_FAILED" then
			return
		end
	elseif start == "SWING" then
		spellID = 6603
	elseif start == "RANGE" then
		spellID = 3018
	else
		return
	end
	npcData_mobID = npcData[mobID]
	if not npcData_mobID then
		npcData_mobID = {}
		npcData[mobID] = npcData_mobID
	end
	local difficulty = GetInstanceDifficulty()
	local npcData_mobID_stat = npcData_mobID["stats" .. difficulty]
	if not npcData_mobID_stat then
		npcData_mobID_stat = {}
		npcData_mobID["stats" .. difficulty] = npcData_mobID_stat
	end
	local npcData_mobID_stat_spells = npcData_mobID_stat.spells
	if not npcData_mobID_stat_spells then
		npcData_mobID_stat_spells = {}
		npcData_mobID_stat.spells = npcData_mobID_stat_spells
	end
	npcData_mobID_stat_spells[spellID] = (npcData_mobID_stat_spells[spellID] or 0) + 1
end

local deserialize__text, deserialize__position
local OnDeserialize
function NPCs:OnSerialize()
	local ZONE = CurseProfiler.getZoneID
	local FACTION = CurseProfiler.getFactionID
	local INT = CurseProfiler.makeSerializedInteger
	local TUPLE = CurseProfiler.makeSerializedTuple
	local ENUM = CurseProfiler.makeSerializedEnum
	local savedItemData, savedObjectData, savedNPCData
	if not deserialize__text then
		savedItemData = {}
		savedObjectData = {}
		savedNPCData = {}
	else
		savedItemData, savedObjectData, savedNPCData = OnDeserialize(deserialize__text, deserialize__position, true)
	end
	
	for itemID, itemData_itemID in pairs(itemData) do
		if not savedItemData[itemID] then
			savedItemData[itemID] = {}
		end
		for spellID, itemData_itemID_spellID in pairs(itemData_itemID) do
			if not savedItemData[itemID][spellID] then
				savedItemData[itemID][spellID] = {}
			end
			for loot in pairs(itemData_itemID_spellID) do
				savedItemData[itemID][spellID][loot] = true
			end
		end
	end
	
	for spellID, objectData_spellID in pairs(objectData) do
		if not savedObjectData[spellID] then
			savedObjectData[spellID] = {}
		end
		for t in pairs(objectData_spellID) do
			savedObjectData[spellID][t] = true
		end
	end
	
	local LibMobHealth = _G.LibStub and _G.LibStub("LibMobHealth-4.0", true)
	for mobID, npcData_mobID in pairs(npcData) do
		local savedNPCData_mobID = savedNPCData[mobID]
		if not savedNPCData_mobID then
			savedNPCData_mobID = {}
			savedNPCData[mobID] = savedNPCData_mobID
		end
		if npcData_mobID.loc then
			if not savedNPCData_mobID.loc then
				savedNPCData_mobID.loc = {}
			end
			for zone, npcData_mobID_loc_zone in pairs(npcData_mobID.loc) do
				if not savedNPCData_mobID.loc[zone] then
					savedNPCData_mobID.loc[zone] = {}
				end
				for coord in pairs(npcData_mobID_loc_zone) do
					savedNPCData_mobID.loc[zone][coord] = true
				end
			end
		end
		
		savedNPCData_mobID.sex = npcData_mobID.sex
		savedNPCData_mobID.faction = npcData_mobID.faction
		if npcData_mobID.reaction then
			if not savedNPCData_mobID.reaction then
				savedNPCData_mobID.reaction = {}
			end
			savedNPCData_mobID.reaction[FigureFaction("player")] = npcData_mobID.reaction
		end
		for difficulty = 1, 3 do
			local npcData_mobID_stat = npcData_mobID["stats" .. difficulty]
			if npcData_mobID_stat then
				local savedNPCData_mobID_stat = savedNPCData_mobID["stats" .. difficulty]
				if not savedNPCData_mobID_stat then
					savedNPCData_mobID_stat = {}
					savedNPCData_mobID["stats" .. difficulty] = savedNPCData_mobID_stat
				end
				if npcData_mobID_stat.levels then
					if not savedNPCData_mobID_stat.levels then
						savedNPCData_mobID_stat.levels = {}
					end
					for level in pairs(npcData_mobID_stat.levels) do
						savedNPCData_mobID_stat.levels[level] = true
					end
				end
				
				if LibMobHealth and savedNPCData_mobID_stat.levels and mobIDToName[mobID] then
					if not savedNPCData_mobID_stat.health then
						savedNPCData_mobID_stat.health = {}
					end
					for level in pairs(savedNPCData_mobID_stat.levels) do
						local hp = LibMobHealth:GetMaxHP(mobIDToName[mobID], level, 'npc', difficulty, true)
						if hp then
							savedNPCData_mobID_stat.health[level] = hp
						elseif level == 0 then
							hp = LibMobHealth:GetMaxHP(mobIDToName[mobID], -1, 'npc', difficulty, true)
							if hp then
								savedNPCData_mobID_stat.health[level] = hp
							end
						end
					end
				end
				
				if npcData_mobID_stat.loot then
					if not savedNPCData_mobID_stat.loot then
						savedNPCData_mobID_stat.loot = {}
					end
					for spellID, npcData_mobID_stat_loot_spellID in pairs(npcData_mobID_stat.loot) do
						if not savedNPCData_mobID_stat.loot[spellID] then
							savedNPCData_mobID_stat.loot[spellID] = {}
						end
						for loot in pairs(npcData_mobID_stat_loot_spellID) do
							savedNPCData_mobID_stat.loot[spellID][loot] = true
						end
					end
				end
				
				if npcData_mobID_stat.spells then
					if not savedNPCData_mobID_stat.spells then
						savedNPCData_mobID_stat.spells = {}
					end
					for spellID, count in pairs(npcData_mobID_stat.spells) do
						savedNPCData_mobID_stat.spells[spellID] = (savedNPCData_mobID_stat.spells[spellID] or 0) + count
					end
				end
			end
		end
		
		if npcData_mobID.merchant then
			if not savedNPCData_mobID.merchant then
				savedNPCData_mobID.merchant = {}
			end
			for item, quantity in pairs(npcData_mobID.merchant) do
				savedNPCData_mobID.merchant[item] = quantity
			end
		end
		if npcData_mobID.limitedMerchant then
			if not savedNPCData_mobID.limitedMerchant then
				savedNPCData_mobID.limitedMerchant = {}
			end
			for item, numAvailable in pairs(npcData_mobID.limitedMerchant) do
				if not savedNPCData_mobID.limitedMerchant[item] or savedNPCData_mobID.limitedMerchant[item] < numAvailable then
					savedNPCData_mobID.limitedMerchant[item] = numAvailable
				end
			end
		end
		if npcData_mobID.merchantHonor then
			if not savedNPCData_mobID.merchantHonor then
				savedNPCData_mobID.merchantHonor = {}
			end
			for item, points in pairs(npcData_mobID.merchantHonor) do
				savedNPCData_mobID.merchantHonor[item] = points
			end
		end
		if npcData_mobID.merchantArena then
			if not savedNPCData_mobID.merchantArena then
				savedNPCData_mobID.merchantArena = {}
			end
			for item, points in pairs(npcData_mobID.merchantArena) do
				savedNPCData_mobID.merchantArena[item] = points
			end
		end
		if npcData_mobID.merchantReqRating then
			if not savedNPCData_mobID.merchantReqRating then
				savedNPCData_mobID.merchantReqRating = {}
			end
			for item, points in pairs(npcData_mobID.merchantReqRating) do
				savedNPCData_mobID.merchantReqRating[item] = points
			end
		end
		for i = 1, 3 do
			if npcData_mobID["merchantItem" .. i] then
				if not savedNPCData_mobID["merchantItem" .. i] then
					savedNPCData_mobID["merchantItem" .. i] = {}
					savedNPCData_mobID["merchantItemCost" .. i] = {}
				end
				for item, points in pairs(npcData_mobID["merchantItem" .. i]) do
					savedNPCData_mobID["merchantItem" .. i][item] = points
					savedNPCData_mobID["merchantItemCost" .. i][item] = npcData_mobID["merchantItemCost" .. i][item]
				end
			end
		end
		
		if npcData_mobID.trainer then
			if not savedNPCData_mobID.trainer then
				savedNPCData_mobID.trainer = {}
			end
			local _, playerClass = _G.UnitClass("player")
			local _, playerRace = _G.UnitRace("player")
			for data in pairs(npcData_mobID.trainer) do
				local name, rank, skill, cost = unpack(data)
				local found = false
				for k in pairs(savedNPCData_mobID.trainer) do
					if k[1] == playerClass and k[2] == playerRace and k[3] == name and k[4] == rank then
						found = true
						k[5] = skill
						k[6] = cost
						break
					end
				end
				if not found then
					savedNPCData_mobID.trainer[{ playerClass, playerRace, unpack(data) }] = true
				end
			end
		end
	end
	
	local fullData = {}
	local fullItemData = {}
	fullData[1] = fullItemData
	for itemID, savedItemData_itemID in pairs(savedItemData) do
		for spellID, savedItemData_itemID_spellID in pairs(savedItemData_itemID) do
			for loot in pairs(savedItemData_itemID_spellID) do
				local lootData = {}
				for id, quantity in pairs(loot) do
					lootData[TUPLE {
						INT("Int32", id),
						INT("Int32", quantity)
					}] = true
				end
				fullItemData[TUPLE {
					INT("Int32", itemID),
					INT("Int32", spellID),
					lootData,
				}] = true
			end
		end
	end
	
	local fullObjectData = {}
	fullData[2] = fullObjectData
	for spellID, savedObjectData_spellID in pairs(savedObjectData) do
		for data in pairs(savedObjectData_spellID) do
			local lootData = {}
			for id, quantity in pairs(data.loot) do
				lootData[TUPLE {
					INT("Int32", id),
					INT("Int32", quantity)
				}] = true
			end
			fullObjectData[TUPLE {
				INT("Int32", spellID),
				ZONE(data.zone or ''),
				INT("Int16", math_floor((data.x or 0) * 10000 + 0.5)),
				INT("Int16", math_floor((data.y or 0) * 10000 + 0.5)),
				lootData,
				data.requires or '',
				data.target or '',
			}] = true
		end
	end
	
	local fullNPCData = {}
	fullData[3] = fullNPCData
	for mobID, savedNPCData_mobID in pairs(savedNPCData) do
		local loc = {}
		if savedNPCData_mobID.loc then
			for zone, savedNPCData_mobID_loc_zone in pairs(savedNPCData_mobID.loc) do
				local t = {}
				for coord in pairs(savedNPCData_mobID_loc_zone) do
					local y = coord % 10001
					local x = (coord - y) / 10001
					t[TUPLE {
						INT("Int16", x),
						INT("Int16", y),
					}] = true
				end
				loc[TUPLE {
					ZONE(zone or ''),
					t
				}] = true
			end
		end
		local reactions = {}
		if savedNPCData_mobID.reaction then
			for faction, reaction in pairs(savedNPCData_mobID.reaction) do
				reactions[TUPLE {
					FACTION(faction or ''),
					INT("Byte", reaction),
				}] = true
			end
		end
		local merchant = {}
		if savedNPCData_mobID.merchant then
			for itemID, quantity in pairs(savedNPCData_mobID.merchant) do
				merchant[TUPLE {
					INT("Int32", itemID),
					INT("Byte", quantity)
				}] = true
			end
		end
		local limitedMerchant = {}
		if savedNPCData_mobID.limitedMerchant then
			for itemID, numAvailable in pairs(savedNPCData_mobID.limitedMerchant) do
				limitedMerchant[TUPLE {
					INT("Int32", itemID),
					INT("Byte", numAvailable)
				}] = true
			end
		end
		local merchantHonorArenaReqRatingItems = {}
		if savedNPCData_mobID.merchantHonor or savedNPCData_mobID.merchantArena or savedNPCData_mobID.merchantReqRating or savedNPCData_mobID.merchantItem1 then
			local itemIDs = {}
			if not savedNPCData_mobID.merchantHonor then
				savedNPCData_mobID.merchantHonor = {}
			else
				for itemID in pairs(savedNPCData_mobID.merchantHonor) do
					itemIDs[itemID] = true
				end
			end
			if not savedNPCData_mobID.merchantArena then
				savedNPCData_mobID.merchantArena = {}
			else
				for itemID in pairs(savedNPCData_mobID.merchantArena) do
					itemIDs[itemID] = true
				end
			end
			if not savedNPCData_mobID.merchantReqRating then
				savedNPCData_mobID.merchantReqRating = {}
			else
				for itemID in pairs(savedNPCData_mobID.merchantReqRating) do
					itemIDs[itemID] = true
				end
			end
			for i = 1, 3 do
				if not savedNPCData_mobID["merchantItem" .. i] then
					savedNPCData_mobID["merchantItem" .. i] = {}
					savedNPCData_mobID["merchantItemCost" .. i] = {}
				else
					for itemID in pairs(savedNPCData_mobID["merchantItem" .. i]) do
						itemIDs[itemID] = true
					end
				end
			end
			for itemID in pairs(itemIDs) do
				merchantHonorArenaReqRatingItems[TUPLE {
					INT("Int32", itemID),
					INT("Int32", savedNPCData_mobID.merchantHonor[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantArena[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantReqRating[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantItem1[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantItemCost1[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantItem2[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantItemCost2[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantItem3[itemID] or 0),
					INT("Int32", savedNPCData_mobID.merchantItemCost3[itemID] or 0),
				}] = true
			end
		end
		
		local trainer = {}
		if savedNPCData_mobID.trainer then
			for training in pairs(savedNPCData_mobID.trainer) do
				local class, race, name, rank, skill, cost = unpack(training)
				trainer[TUPLE {
					ENUM("class", class),
					ENUM("race", race),
					name,
					rank,
					skill,
					INT("Int32", cost)
				}] = true
			end
		end
		
		local stats = {}
		for difficulty = 1, 3 do
			local stat = { {}, {}, {} }
			local savedNPCData_mobID_stat = savedNPCData_mobID["stats" .. difficulty]
			if savedNPCData_mobID_stat then
				if savedNPCData_mobID_stat.levels then
					for level in pairs(savedNPCData_mobID_stat.levels) do
						stat[1][TUPLE {
							INT("Byte", level),
							INT("Int32", savedNPCData_mobID_stat.health and savedNPCData_mobID_stat.health[level] or 0),
						}] = true
					end
				end
				if savedNPCData_mobID_stat.loot then
					for spellID, savedNPCData_mobID_stat_loot_spellID in pairs(savedNPCData_mobID_stat.loot) do
						local t = {}
						for loot in pairs(savedNPCData_mobID_stat_loot_spellID) do
							local lootData = {}
							for id, quantity in pairs(loot) do
								lootData[TUPLE {
									INT("Int32", id),
									INT("Int32", quantity)
								}] = true
							end
							t[lootData] = true
						end
						stat[2][TUPLE {
							INT("Int32", spellID),
							t
						}] = true
					end
				end
				if savedNPCData_mobID_stat.spells then
					for spellID, count in pairs(savedNPCData_mobID_stat.spells) do
						stat[3][TUPLE {
							INT("Int32", spellID),
							INT("Int32", count)
						}] = true
					end
				end
			end
			stats[difficulty] = TUPLE(stat)
		end
		
		fullNPCData[TUPLE {
			INT("Int32", mobID),
			INT("Byte", savedNPCData_mobID.sex or 0),
			FACTION(savedNPCData_mobID.faction or ''),
			loc,
			reactions,
			merchant,
			limitedMerchant,
			merchantHonorArenaReqRatingItems,
			trainer,
			stats[1],
			stats[2],
			stats[3],
		}] = true
	end
	return TUPLE(fullData)
end

function OnDeserialize(text, position, run)
	local deserialize = CurseProfiler.deserialize
	
	local itemData, objectData, npcData
	if run then
		itemData = {}
		objectData = {}
		npcData = {}
	end
	
	local num
	position, num = deserialize('list', text, position)
	
	for i = 1, num do
		local itemID, spellID, lootData
		position, itemID = deserialize('Int32', text, position, not run)
		position, spellID = deserialize('Int32', text, position, not run)
		local lootNum
		position, lootNum = deserialize('list', text, position)
		if run then
			lootData = {}
		end
		for j = 1, lootNum do
			local id, quantity
			position, id = deserialize('Int32', text, position, not run)
			position, quantity = deserialize('Int32', text, position, not run)
			if run then
				lootData[id] = quantity
			end
		end
		if run then
			local itemData_itemID = itemData[itemID]
			if not itemData_itemID then
				itemData_itemID = {}
				itemData[itemID] = itemData_itemID
			end
			local itemData_itemID_spellID = itemData_itemID[spellID]
			if not itemData_itemID_spellID then
				itemData_itemID_spellID = {}
				itemData_itemID[spellID] = itemData_itemID_spellID
			end
			itemData_itemID_spellID[lootData] = true
		end
	end
	
	local num
	position, num = deserialize('list', text, position)
	
	for i = 1, num do
		local spellID, zone, x, y, lootData, requires, target
		position, spellID = deserialize('Int32', text, position, not run)
		position, zone = deserialize('zone', text, position, not run)
		position, x = deserialize('Int16', text, position, not run)
		position, y = deserialize('Int16', text, position, not run)
		local lootNum
		position, lootNum = deserialize('list', text, position)
		if run then
			lootData = {}
		end
		for j = 1, lootNum do
			local id, quantity
			position, id = deserialize('Int32', text, position, not run)
			position, quantity = deserialize('Int32', text, position, not run)
			if run then
				lootData[id] = quantity
			end
		end
		position, requires = deserialize('string', text, position, not run)
		position, target = deserialize('string', text, position, not run)
		if run then
			local objectData_spellID = objectData[spellID]
			if not objectData_spellID then
				objectData_spellID = {}
				objectData[spellID] = objectData_spellID
			end
			objectData_spellID[{
				x = x / 10000,
				y = y / 10000,
				zone = zone,
				loot = lootData,
				requires = requires,
				target = target
			}] = true
		end
	end
	
	local num
	position, num = deserialize('list', text, position)
	for i = 1, num do
		local mobID, sex, faction, loc, reactions, merchant, limitedMerchant, merchantHonor, merchantArena, merchantReqRating, merchantItem1, merchantItemCost1, merchantItem2, merchantItemCost2, merchantItem3, merchantItemCost3, trainer, stats
		
		position, mobID = deserialize('Int32', text, position, not run)
		position, sex = deserialize('Byte', text, position, not run)
		position, faction = deserialize('faction', text, position, not run)
		local locNum
		position, locNum = deserialize('list', text, position)
		if run then
			loc = {}
		end
		for i = 1, locNum do
			local zone
			position, zone = deserialize('zone', text, position, not run)
			if run then
				loc[zone] = {}
			end
			local numCoords
			position, numCoords = deserialize('list', text, position)
			for j = 1, numCoords do
				local x, y
				position, x = deserialize('Int16', text, position, not run)
				position, y = deserialize('Int16', text, position, not run)
				if run then
					loc[zone][x * 10001 + y] = true
				end
			end
		end
		local reactionNum
		position, reactionNum = deserialize('list', text, position)
		for i = 1, reactionNum do
			local faction, reaction
			position, faction = deserialize('faction', text, position, not run)
			position, reaction = deserialize('Byte', text, position, not run)
			if run then
				if not reactions then
					reactions = {}
				end
				reactions[faction] = reaction
			end
		end
		local merchantNum
		position, merchantNum = deserialize('list', text, position)
		for i = 1, merchantNum do
			local itemID, quantity
			position, itemID = deserialize('Int32', text, position, not run)
			position, quantity = deserialize('Byte', text, position, not run)
			if run then
				if not merchant then
					merchant = {}
				end
				merchant[itemID] = quantity
			end
		end
		local limitedMerchantNum
		position, limitedMerchantNum = deserialize('list', text, position)
		for i = 1, limitedMerchantNum do
			local itemID, numAvailable
			position, itemID = deserialize('Int32', text, position, not run)
			position, numAvailable = deserialize('Byte', text, position, not run)
			if run then
				if not limitedMerchant then
					limitedMerchant = {}
				end
				limitedMerchant[itemID] = numAvailable
			end
		end
		local merchantHonorArenaReqRatingItemsNum
		position, merchantHonorArenaReqRatingItemsNum = deserialize('list', text, position)
		for i = 1, merchantHonorArenaReqRatingItemsNum do
			local itemID, honor, arena, reqRating, item1, itemCost1, item2, itemCost2, item3, itemCost3
			position, itemID = deserialize('Int32', text, position, not run)
			position, honor = deserialize('Int32', text, position, not run)
			position, arena = deserialize('Int32', text, position, not run)
			position, reqRating = deserialize('Int32', text, position, not run)
			position, item1 = deserialize('Int32', text, position, not run)
			position, itemCost1 = deserialize('Int32', text, position, not run)
			position, item2 = deserialize('Int32', text, position, not run)
			position, itemCost2 = deserialize('Int32', text, position, not run)
			position, item3 = deserialize('Int32', text, position, not run)
			position, itemCost3 = deserialize('Int32', text, position, not run)
			if run then
				if honor ~= 0 then
					if not merchantHonor then
						merchantHonor = {}
					end
					merchantHonor[itemID] = honor
				end
				if arena ~= 0 then
					if not merchantArena then
						merchantArena = {}
					end
					merchantArena[itemID] = arena
				end
				if reqRating ~= 0 then
					if not merchantReqRating then
						merchantReqRating = {}
					end
					merchantReqRating[itemID] = reqRating
				end
				if item1 ~= 0 and itemCost1 ~= 0 then
					if not merchantItem1 then
						merchantItem1 = {}
						merchantItemCost1 = {}
					end
					merchantItem1[itemID] = item1
					merchantItemCost1[itemID] = itemCost1
				end
				if item2 ~= 0 and itemCost2 ~= 0 then
					if not merchantItem2 then
						merchantItem2 = {}
						merchantItemCost2 = {}
					end
					merchantItem2[itemID] = item2
					merchantItemCost2[itemID] = itemCost2
				end
				if item3 ~= 0 and itemCost3 ~= 0 then
					if not merchantItem3 then
						merchantItem3 = {}
						merchantItemCost3 = {}
					end
					merchantItem3[itemID] = item3
					merchantItemCost3[itemID] = itemCost3
				end
			end
		end
		local trainerNum
		position, trainerNum = deserialize('list', text, position)
		for i = 1, trainerNum do
			local class, race, name, rank, skill, cost
			position, class = deserialize('enum-class', text, position, not run)
			position, race = deserialize('enum-race', text, position, not run)
			position, name = deserialize('string', text, position, not run)
			position, rank = deserialize('string', text, position, not run)
			position, skill = deserialize('string', text, position, not run)
			position, cost = deserialize('Int32', text, position, not run)
			if run then
				if not trainer then
					trainer = {}
				end
				trainer[{ class, race, name, rank, skill, cost }] = true
			end
		end
		
		if run then
			stats = {}
		end
		for difficulty = 1, 3 do
			local stat
			if run then
				stat = {}
			end
			local levelNum
			position, levelNum = deserialize('list', text, position)
			for i = 1, levelNum do
				local level, hp
				position, level = deserialize('Byte', text, position, not run)
				position, hp = deserialize('Int32', text, position, not run)
				if run then
					if not stat.levels then
						stat.levels = {}
					end
					stat.levels[level] = true
					if hp ~= 0 then
						if not stat.health then
							stat.health = {}
						end
						stat.health[level] = hp
					end
				end
			end
			local lootSpellNum
			position, lootSpellNum = deserialize('list', text, position)
			for i = 1, lootSpellNum do
				local spellID
				position, spellID = deserialize('Int32', text, position, not run)
				if run then
					if not stat.loot then
						stat.loot = {}
					end
					stat.loot[spellID] = {}
				end
				local lootNum
				position, lootNum = deserialize('list', text, position)
				for j = 1, lootNum do
					local lootData
					if run then
						lootData = {}
						stat.loot[spellID][lootData] = true
					end
					local itemNum
					position, itemNum = deserialize('list', text, position)
					for k = 1, itemNum do
						local itemID, quantity
						position, itemID = deserialize('Int32', text, position, not run)
						position, quantity = deserialize('Int32', text, position, not run)
						if run then
							lootData[itemID] = quantity
						end
					end
				end
			end
			local spellNum
			position, spellNum = deserialize('list', text, position)
			for i = 1, spellNum do
				local spellID, count
				position, spellID = deserialize('Int32', text, position, not run)
				position, count = deserialize('Int32', text, position, not run)
				if run then
					if not stat.spells then
						stat.spells = {}
					end
					stat.spells[spellID] = count
				end
			end
			if run and next(stat) then
				stats[difficulty] = stat
			end
		end
		if run then
			npcData[mobID] = {
				sex = sex,
				faction = faction,
				loc = loc,
				reactions = reactions,
				merchant = merchant,
				limitedMerchant = limitedMerchant,
				merchantHonor = merchantHonor,
				merchantArena = merchantArena,
				merchantReqRating = merchantReqRating,
				merchantItem1 = merchantItem1,
				merchantItemCost1 = merchantItemCost1,
				merchantItem2 = merchantItem2,
				merchantItemCost2 = merchantItemCost2,
				merchantItem3 = merchantItem3,
				merchantItemCost3 = merchantItemCost3,
				trainer = trainer,
				stats1 = stats[1],
				stats2 = stats[2],
				stats3 = stats[3],
			}
		end
	end
	
	if run then
		return itemData, objectData, npcData
	else
		return position
	end
end

function NPCs:OnDeserialize(text, position)
	deserialize__text, deserialize__position = text, position
	return OnDeserialize(text, position, false)
end

function NPCs:ClearCache()
	wipe(npcData)
	wipe(itemData)
	wipe(objectData)
	wipe(lootCD)
	wipe(mobIDToName)
	wipe(nameToMobID)
end
